﻿/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using System.Globalization;

namespace Raven.Client.Json.Serialization.NewtonsoftJson.Internal.Converters
{
    /// <summary> Provides support for converting dates to strings and vice-versa.
    /// The strings are structured so that lexicographic sorting orders
    /// them by date, which makes them suitable for use as field values
    /// and search terms.
    ///
    /// <p/>This class also helps you to limit the resolution of your dates. Do not
    /// save dates with a finer resolution than you really need, as then
    /// RangeQuery and PrefixQuery will require more memory and become slower.
    ///
    /// <p/>Compared to {@link DateField} the strings generated by the methods
    /// in this class take slightly more space, unless your selected resolution
    /// is set to <code>Resolution.DAY</code> or lower.
    ///
    /// <p/>
    /// Another approach is {@link NumericUtils}, which provides
    /// a sortable binary representation (prefix encoded) of numeric values, which
    /// date/time are.
    /// For indexing a {@link Date} or {@link Calendar}, just get the Unix timestamp as
    /// <code>long</code> using {@link Date#getTime} or {@link Calendar#getTimeInMillis} and
    /// index this as a numeric value with {@link NumericField}
    /// and use {@link NumericRangeQuery} to query it.
    /// </summary>
    internal sealed class DateTools
    {
        private static readonly string YEAR_FORMAT = "yyyy";
        private static readonly string MONTH_FORMAT = "yyyyMM";
        private static readonly string DAY_FORMAT = "yyyyMMdd";
        private static readonly string HOUR_FORMAT = "yyyyMMddHH";
        private static readonly string MINUTE_FORMAT = "yyyyMMddHHmm";
        private static readonly string SECOND_FORMAT = "yyyyMMddHHmmss";
        private static readonly string MILLISECOND_FORMAT = "yyyyMMddHHmmssfff";

        // cannot create, the class has static methods only
        private DateTools()
        {
        }

        /// <summary> Converts a Date to a string suitable for indexing.
        ///
        /// </summary>
        /// <param name="date">the date to be converted
        /// </param>
        /// <param name="resolution">the desired resolution, see
        /// {@link #Round(Date, DateTools.Resolution)}
        /// </param>
        /// <returns> a string in format <code>yyyyMMddHHmmssSSS</code> or shorter,
        /// depending on <code>resolution</code>; using GMT as timezone
        /// </returns>
        public static string DateToString(DateTime date, Resolution resolution)
        {
            return TimeToString(date.Ticks / TimeSpan.TicksPerMillisecond, resolution);
        }

        /// <summary> Converts a millisecond time to a string suitable for indexing.
        ///
        /// </summary>
        /// <param name="time">the date expressed as milliseconds since January 1, 1970, 00:00:00 GMT
        /// </param>
        /// <param name="resolution">the desired resolution, see
        /// {@link #Round(long, DateTools.Resolution)}
        /// </param>
        /// <returns> a string in format <code>yyyyMMddHHmmssSSS</code> or shorter,
        /// depending on <code>resolution</code>; using GMT as timezone
        /// </returns>
        public static string TimeToString(long time, Resolution resolution)
        {
            DateTime date = new DateTime(Round(time, resolution));

            if (resolution == Resolution.YEAR)
            {
                return date.ToString(YEAR_FORMAT, CultureInfo.InvariantCulture);
            }
            if (resolution == Resolution.MONTH)
            {
                return date.ToString(MONTH_FORMAT, CultureInfo.InvariantCulture);
            }
            if (resolution == Resolution.DAY)
            {
                return date.ToString(DAY_FORMAT, CultureInfo.InvariantCulture);
            }
            if (resolution == Resolution.HOUR)
            {
                return date.ToString(HOUR_FORMAT, CultureInfo.InvariantCulture);
            }
            if (resolution == Resolution.MINUTE)
            {
                return date.ToString(MINUTE_FORMAT, CultureInfo.InvariantCulture);
            }
            if (resolution == Resolution.SECOND)
            {
                return date.ToString(SECOND_FORMAT, CultureInfo.InvariantCulture);
            }
            if (resolution == Resolution.MILLISECOND)
            {
                return date.ToString(MILLISECOND_FORMAT, CultureInfo.InvariantCulture);
            }

            throw new ArgumentException("unknown resolution " + resolution);
        }

        /// <summary> Converts a string produced by <code>timeToString</code> or
        /// <code>DateToString</code> back to a time, represented as the
        /// number of milliseconds since January 1, 1970, 00:00:00 GMT.
        ///
        /// </summary>
        /// <param name="dateString">the date string to be converted
        /// </param>
        /// <returns> the number of milliseconds since January 1, 1970, 00:00:00 GMT
        /// </returns>
        /// <throws>  ParseException if <code>dateString</code> is not in the  </throws>
        /// <summary>  expected format
        /// </summary>
        public static long StringToTime(string dateString)
        {
            return StringToDate(dateString).Ticks;
        }

        /// <summary> Converts a string produced by <code>timeToString</code> or
        /// <code>DateToString</code> back to a time, represented as a
        /// Date object.
        ///
        /// </summary>
        /// <param name="dateString">the date string to be converted
        /// </param>
        /// <returns> the parsed time as a Date object
        /// </returns>
        /// <throws>  ParseException if <code>dateString</code> is not in the  </throws>
        /// <summary>  expected format
        /// </summary>
        public static DateTime StringToDate(string dateString)
        {
            DateTime date;
            if (dateString.Length == 4)
            {
                date = new DateTime(Convert.ToInt16(dateString.Substring(0, 4)),
                    1, 1, 0, 0, 0, 0);
            }
            else if (dateString.Length == 6)
            {
                date = new DateTime(Convert.ToInt16(dateString.Substring(0, 4)),
                    Convert.ToInt16(dateString.Substring(4, 2)),
                    1, 0, 0, 0, 0);
            }
            else if (dateString.Length == 8)
            {
                date = new DateTime(Convert.ToInt16(dateString.Substring(0, 4)),
                    Convert.ToInt16(dateString.Substring(4, 2)),
                    Convert.ToInt16(dateString.Substring(6, 2)),
                    0, 0, 0, 0);
            }
            else if (dateString.Length == 10)
            {
                date = new DateTime(Convert.ToInt16(dateString.Substring(0, 4)),
                    Convert.ToInt16(dateString.Substring(4, 2)),
                    Convert.ToInt16(dateString.Substring(6, 2)),
                    Convert.ToInt16(dateString.Substring(8, 2)),
                    0, 0, 0);
            }
            else if (dateString.Length == 12)
            {
                date = new DateTime(Convert.ToInt16(dateString.Substring(0, 4)),
                    Convert.ToInt16(dateString.Substring(4, 2)),
                    Convert.ToInt16(dateString.Substring(6, 2)),
                    Convert.ToInt16(dateString.Substring(8, 2)),
                    Convert.ToInt16(dateString.Substring(10, 2)),
                    0, 0);
            }
            else if (dateString.Length == 14)
            {
                date = new DateTime(Convert.ToInt16(dateString.Substring(0, 4)),
                    Convert.ToInt16(dateString.Substring(4, 2)),
                    Convert.ToInt16(dateString.Substring(6, 2)),
                    Convert.ToInt16(dateString.Substring(8, 2)),
                    Convert.ToInt16(dateString.Substring(10, 2)),
                    Convert.ToInt16(dateString.Substring(12, 2)),
                    0);
            }
            else if (dateString.Length == 17)
            {
                date = new DateTime(Convert.ToInt16(dateString.Substring(0, 4)),
                    Convert.ToInt16(dateString.Substring(4, 2)),
                    Convert.ToInt16(dateString.Substring(6, 2)),
                    Convert.ToInt16(dateString.Substring(8, 2)),
                    Convert.ToInt16(dateString.Substring(10, 2)),
                    Convert.ToInt16(dateString.Substring(12, 2)),
                    Convert.ToInt16(dateString.Substring(14, 3)));
            }
            else
            {
                throw new FormatException("Input is not valid date string: " + dateString);
            }
            return date;
        }

        /// <summary>
        /// Limit a date's resolution. For example, the date <code>2004-09-21 13:50:11</code>
        /// will be changed to <code>2004-09-01 00:00:00</code> when using
        /// <code>Resolution.MONTH</code>.
        /// </summary>
        /// <param name="date">The date.</param>
        /// <param name="resolution">The desired resolution of the date to be returned</param>
        /// <returns>
        /// the date with all values more precise than <code>resolution</code>
        /// set to 0 or 1
        /// </returns>
        public static DateTime Round(DateTime date, Resolution resolution)
        {
            return new DateTime(Round(date.Ticks / TimeSpan.TicksPerMillisecond, resolution), date.Kind);
        }

        /// <summary> Limit a date's resolution. For example, the date <code>1095767411000</code>
        /// (which represents 2004-09-21 13:50:11) will be changed to
        /// <code>1093989600000</code> (2004-09-01 00:00:00) when using
        /// <code>Resolution.MONTH</code>.
        ///
        /// </summary>
        /// <param name="time">The time in milliseconds (not ticks).</param>
        /// <param name="resolution">The desired resolution of the date to be returned
        /// </param>
        /// <returns> the date with all values more precise than <code>resolution</code>
        /// set to 0 or 1, expressed as milliseconds since January 1, 1970, 00:00:00 GMT
        /// </returns>
        public static long Round(long time, Resolution resolution)
        {
            DateTime dt = new DateTime(time * TimeSpan.TicksPerMillisecond);

            if (resolution == Resolution.YEAR)
            {
                dt = dt.AddMonths(1 - dt.Month);
                dt = dt.AddDays(1 - dt.Day);
                dt = dt.AddHours(0 - dt.Hour);
                dt = dt.AddMinutes(0 - dt.Minute);
                dt = dt.AddSeconds(0 - dt.Second);
                dt = dt.AddMilliseconds(0 - dt.Millisecond);
            }
            else if (resolution == Resolution.MONTH)
            {
                dt = dt.AddDays(1 - dt.Day);
                dt = dt.AddHours(0 - dt.Hour);
                dt = dt.AddMinutes(0 - dt.Minute);
                dt = dt.AddSeconds(0 - dt.Second);
                dt = dt.AddMilliseconds(0 - dt.Millisecond);
            }
            else if (resolution == Resolution.DAY)
            {
                dt = dt.AddHours(0 - dt.Hour);
                dt = dt.AddMinutes(0 - dt.Minute);
                dt = dt.AddSeconds(0 - dt.Second);
                dt = dt.AddMilliseconds(0 - dt.Millisecond);
            }
            else if (resolution == Resolution.HOUR)
            {
                dt = dt.AddMinutes(0 - dt.Minute);
                dt = dt.AddSeconds(0 - dt.Second);
                dt = dt.AddMilliseconds(0 - dt.Millisecond);
            }
            else if (resolution == Resolution.MINUTE)
            {
                dt = dt.AddSeconds(0 - dt.Second);
                dt = dt.AddMilliseconds(0 - dt.Millisecond);
            }
            else if (resolution == Resolution.SECOND)
            {
                dt = dt.AddMilliseconds(0 - dt.Millisecond);
            }
            else if (resolution == Resolution.MILLISECOND)
            {
                // don't cut off anything
            }
            else
            {
                throw new ArgumentException("unknown resolution " + resolution);
            }
            return dt.Ticks;
        }

        /// <summary>Specifies the time granularity. </summary>
        internal sealed class Resolution
        {
            /// <summary>
            /// Resolution by year
            /// </summary>
            public static readonly Resolution YEAR = new Resolution("year");

            /// <summary>
            /// Resolution by month
            /// </summary>
            public static readonly Resolution MONTH = new Resolution("month");

            /// <summary>
            /// Resolution by day
            /// </summary>
            public static readonly Resolution DAY = new Resolution("day");

            /// <summary>
            /// Resolution by hour
            /// </summary>
            public static readonly Resolution HOUR = new Resolution("hour");

            /// <summary>
            /// Resolution by minute
            /// </summary>
            public static readonly Resolution MINUTE = new Resolution("minute");

            /// <summary>
            /// Resolution by second
            /// </summary>
            public static readonly Resolution SECOND = new Resolution("second");

            /// <summary>
            /// Resolution by millisecond
            /// </summary>
            public static readonly Resolution MILLISECOND = new Resolution("millisecond");

            private readonly string _resolution;

            internal Resolution()
            {
            }

            internal Resolution(string resolution)
            {
                _resolution = resolution;
            }

            /// <summary>
            /// Returns a <see cref="string"/> that represents this instance.
            /// </summary>
            /// <returns>
            /// A <see cref="string"/> that represents this instance.
            /// </returns>
            public override string ToString()
            {
                return _resolution;
            }
        }

        static DateTools()
        {
            {
                // times need to be normalized so the value doesn't depend on the
                // location the index is created/used:
                // {{Aroush-2.1}}
                /*
                YEAR_FORMAT.setTimeZone(GMT);
                MONTH_FORMAT.setTimeZone(GMT);
                DAY_FORMAT.setTimeZone(GMT);
                HOUR_FORMAT.setTimeZone(GMT);
                MINUTE_FORMAT.setTimeZone(GMT);
                SECOND_FORMAT.setTimeZone(GMT);
                MILLISECOND_FORMAT.setTimeZone(GMT);
                */
            }
        }
    }
}